package dkbta.processing.data;

import static dkbta.util.Utils.enumForName;

import java.io.BufferedReader;
import java.io.EOFException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import dkbta.Config;
import dkbta.ontology.Ontology;
import dkbta.ontology.elements.Element;
import dkbta.ontology.elements.ElementDef;
import dkbta.ontology.elements.ElementType;
import dkbta.processing.Patient;
import dkbta.processing.Patients;
import dkbta.processing.UnknownPatientException;
import dkbta.util.Log;
import dkbta.util.Utils;
import dkbta.util.time.TimeUtils;

/**
 * A data reader that gets its input from an input stream
 * 
 * @author Uri Kanonov
 */
public class InputStreamDataReader{
	public enum ValidElements{
		/** All elements are valid */
		ALL,
		/** Only the required primitives and events */
		REQUIRED_PRIMITIVES_AND_EVENTS,
		/** Only the remote elements */
		REMOTE_ELEMENTS
	}

	/** Whether invalid elements should be logged */
	private static final boolean LOG_INVALID_ELEMENTS = Config.getBoolean(
		"LOG_INVALID_ELEMENTS", true);

	/** A buffered reader wrapper over the input stream */
	private final BufferedReader _reader;

	/** A reference to the ontology */
	private final Ontology _ontology;

	/** A reference to the global patients object */
	private final Patients _patients;

	/**
	 * Whether the first thing to be read from the input stream is the expected number of
	 * lines to be read from the input stream (excluding the number itself which is on a
	 * separate line)
	 */
	private final boolean _startsWithExpectedNumOfLines;

	/** The last patient whose elements have been read */
	private Patient _patient;

	/** The last element that has been read */
	private ElementDef _element;

	/** The valid elements per patient */
	private final Map<Integer, Set<ElementDef>> _validElements;

	/** The kind of elements that are considered valid during parsing */
	private final ValidElements _validElementsType;

	/**
	 * Initializes a DataParser with an input stream from which to read the data
	 * 
	 * @param ontology Reference to the global ontology
	 * @param is The input stream from which to read the data
	 * @param patients All of the patients
	 * @param startsWithExpectedNumOfLines Whether the first thing to be read from the
	 *        input stream is the expected number of lines to be read from the input
	 *        stream (excluding the number itself which is on a separate line)
	 * @param validElementsType The kind of elements that are considered valid during
	 *        parsing
	 */
	public InputStreamDataReader(Ontology ontology, InputStream is, Patients patients,
		boolean startsWithExpectedNumOfLines, ValidElements validElementsType){
		_ontology = ontology;
		_patients = patients;
		_startsWithExpectedNumOfLines = startsWithExpectedNumOfLines;
		_validElementsType = validElementsType;
		_reader = new BufferedReader(new InputStreamReader(is));
		_validElements = new HashMap<Integer, Set<ElementDef>>();
	}

	/**
	 * Fully reads the data from the input stream
	 * 
	 * @throws DataException If an error has occured during the reading
	 */
	public void read() throws DataException{
		Log log = Log.getLogger();

		try{
			// Obtaining the number of expected lines in the input stream
			int expectedNumOfLines = -1;
			if (_startsWithExpectedNumOfLines){
				String expectedNumOfLinesStr = _reader.readLine();
				try{
					expectedNumOfLines = Integer.valueOf(expectedNumOfLinesStr);
				}catch(NumberFormatException e){
					throw new DataException("Invalid expected num of lines: "
							+ expectedNumOfLinesStr);
				}
			}

			String line;
			if (expectedNumOfLines == 0){
				line = _reader.readLine();
				parsePatientAndElement(line, false);
			}else{
				// Reading the data
				boolean duringComment = false;
				int lineCount = 0;
				while ((expectedNumOfLines < 0 || lineCount < expectedNumOfLines)
						&& (line = _reader.readLine()) != null){
					if (line.contains("/*")){
						duringComment = true;
						continue;
					}else if (line.contains("*/")){
						duringComment = false;
						continue;
					}
					if (duringComment || line.equals("") || line.startsWith("//")){
						continue;
					}

					try{
						++lineCount;
						parsePatientAndElement(line, true);
					}catch(Exception e){
						if (LOG_INVALID_ELEMENTS){
							log.finer("Unable to parse element: " + line, e);
						}
					}
				}
				if (expectedNumOfLines > 0 && lineCount < expectedNumOfLines){
					throw new EOFException("Expected " + expectedNumOfLines
							+ " lines however stream only contained " + lineCount
							+ " lines");
				}
			}
			_reader.close();
		}catch(Exception e){
			throw new DataException("Unable to parse data", e);
		}
	}

	private static int POS_PATIENT_ID = 0;

	private static int POS_ELEMENT_TYPE = 1;

	private static int POS_ELEMENT_NAME = 2;

	private static int POS_START_TIME = 3;

	private static int POS_END_TIME = 4;

	private static int POS_VALUE = 5;

	/**
	 * Parses the patient and element parts of the line
	 * 
	 * @param line The line
	 * @param isFullElement Whether the line represents a full element and times and value
	 *        should also be parsed
	 * @throws DataException If an error occurs during the parsing
	 * @throws UnknownPatientException If an element refers to an unknown patient
	 */
	private void parsePatientAndElement(String line, boolean isFullElement)
			throws DataException, UnknownPatientException{
		String[] tokens = line.trim().split(" *, *");

		if (tokens.length < POS_START_TIME){
			throw new DataException("Invalid data, missing components: " + line);
		}

		// The patient-id
		int pid;
		try{
			pid = Integer.parseInt(tokens[POS_PATIENT_ID]);
		}catch(NumberFormatException e){
			throw new DataException("Invalid patient ID: " + line);
		}
		_patient = _patients.getPatient(pid);

		// The element type
		ElementType eType = enumForName(ElementType.class, tokens[POS_ELEMENT_TYPE]);

		// The element name
		String elementName = tokens[POS_ELEMENT_NAME];

		// The element definition
		_element = _ontology.getElement(eType, elementName);
		if (_element == null){
			throw new DataException("Unknown " + eType + ": " + elementName);
		}
		if (!isElementValid(pid)){
			return;
		}

		if (isFullElement){
			parseElement(line, tokens);
		}
	}

	private boolean isElementValid(int pid){
		if (_validElementsType == ValidElements.ALL){
			return true;
		}

		Set<ElementDef> validElements = _validElements.get(pid);
		if (validElements == null){
			switch (_validElementsType){
				case REMOTE_ELEMENTS:
					validElements = _patient.getRemoteElements();
					break;
				case REQUIRED_PRIMITIVES_AND_EVENTS:
					validElements = _patient.getRequiredPrimsAndEvents();
					break;
				default:
					throw new UnsupportedOperationException(
							"Unhandled valid elements type: " + _validElementsType);

			}
			_validElements.put(pid, validElements);
		}
		return validElements.contains(_element);
	}

	/**
	 * Processes the line and creates the element
	 */
	private void parseElement(String line, String[] tokens) throws DataException{
		if (tokens.length < POS_VALUE){
			throw new DataException("Invalid data, missing components: " + line);
		}

		// Parsing the start-end times
		Long startTime = parseTime(tokens[POS_START_TIME]);
		Long endTime = parseTime(tokens[POS_END_TIME]);
		if (startTime == null || endTime == null || endTime < startTime){
			throw new DataException("Invalid data, bad start/end time: " + line);
		}

		// The value
		String[] value = Arrays.copyOfRange(tokens, POS_VALUE, tokens.length);

		// Creating the instance
		try{
			Element e = _element.newInstance(startTime, endTime, value);
			_patient.add(e);
		}catch(Throwable t){
			throw new DataException("Unable to instantiate " + _element, t);
		}
	}
	
	private Long parseTime(String str){
		if (Utils.isDigitsOnly(str)){
			return TimeUtils.parseMillis(str);
		}else{
			return TimeUtils.parseDate(str);
		}
	}

	/**
	 * Returns the last element that has been read
	 * 
	 * @return The last element that has been read
	 */
	public ElementDef getElement(){
		return _element;
	}

	/**
	 * Returns the last patient whose elements have been read
	 * 
	 * @return The last patient whose elements have been read
	 */
	public Patient getPatient(){
		return _patient;
	}
}